package no.uio.ifi.cop.parse.option;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

/**
 *
 * Implements command-line parsing.
 *
 * This implementation does not follow the guidelines
 * of the regular GNU getopt. Long options are allowed 
 * to start with single '-' for instance. Typical use:
 *
 * <pre>
 *
 * GetOpt getopt = new GetOpt();
 *
 * getopt.addStringOption("-f -file --file --file=");
 * getopt.addStringOption("-I");
 * getopt.addBoolOption("-h -help --help -hilfe");
 * getopt.addBoolOption("-v -version");
 *
 * String[] remaining = getopt.parse(argv);
 *
 * if(getopt.isSet("-help")){
 *     System.out.println(" help is on the way ");
 * }
 *
 * if(getopt.isSet("-version")){
 *     System.out.println(" version 1.0");
 * }
 *
 * if(getopt.isSet("-file")){
 *    System.out.println("-file :"+getopt.getValue("-f"));
 * }
 *
 * if(getopt.isSet("-I")){
 *    for(String lib : getopt.getValues("-I")){
 *       System.out.println(" lib: "+lib);
 *    }
 * }
 *
 * for( String s: remaining ){
 *    System.out.println(" not option: "+s);
 * }
 *
 * </pre>
 * 
 *
 * @version 1.0
 * @author  bjarneh@ifi.uio.no
 */

public class GetOpt{

    boolean exitOnMissing;
    HashMap<String, Option> options;

    public GetOpt(){
        this(true);
    }

    public GetOpt(boolean exitOnMissingArgument){
        exitOnMissing = exitOnMissingArgument;
        options = new HashMap<String, Option>();
    }

    /**
     * Set all options back to false or empty.
     */
    public void clear(){
        for(Option o : options.values()){
            o.clear();
        }
    }

    /**
     *
     * Initialize a String option.
     *
     * These can take one or more arguments,
     * to get multiple arguments they must be given
     * several times, like this -I/some/dir -I/other/dir.
     *
     * @param optstring should be a whitespace
     * separated String of all options relevant
     * for a given option.
     *
     * Example "-f -file -file="
     */
    public void addStringOption(String optstring){

        if( optstring == null ){
            throw new RuntimeException("GetOpt.addStringOption( null ) -> fail");
        }

        String[] optarray = optstring.trim().split("\\s+");
        Option opt = new StringOption(optarray);

        for(String o : optarray){
            options.put(o, opt);
        }
    }

    /**
     * Initialize a boolean option.
     *
     * Initialized to false, will be true
     * if given on command line.
     *
     * @param optstring should be a whitespace
     * separated String of all options relevant
     * for a given option.
     *
     * Example "-h -help --help"
     */
    public void addBoolOption(String optstring){

        if( optstring == null ){
            throw new RuntimeException("GetOpt.addStringOption( null ) -> fail");
        }

        String[] optarray = optstring.trim().split("\\s+");
        Option opt = new BoolOption(optarray);

        for(String o : optarray){
            options.put(o, opt);
        }
    }

    /**
     * Parse command line arguments.
     *
     * @param argv command line arguments
     * @return String[] command line arguments
     * which were not options or arguments to
     * options.
     */
    public String[] parse(String[] argv){

        Option tmp;
        StringOption stmp;
        BoolOption btmp;

        int deleted = 0;
        int i = 0;

        while( i < argv.length ){

            tmp = isOption( argv[i] );

            if (tmp != null){

                if( tmp instanceof StringOption ){

                    if ( i + 1 >= argv.length ){

                        System.err.println(" missing argument for: "+argv[i]);
                        if (exitOnMissing){ System.exit(1); }

                    }else{

                        i++;
                        stmp = (StringOption) tmp;
                        stmp.add(argv[i]);

                        argv[i-1] = null;
                        argv[i] = null;
                        deleted += 2;
                    }

                }else if( tmp instanceof BoolOption ){
                    btmp = (BoolOption) tmp;
                    btmp.set();
                    argv[i] = null;
                    deleted++;
                }

            }else if( argv[i].startsWith("-") ){

                if (juxtaLong(argv[i]) || juxtaShort(argv[i])){
                    argv[i] = null;
                    deleted++;
                }
            }

            i++;
        }

        if ( (argv.length - deleted) > 0 ){

            int cnt = 0;
            String[] args = new String[argv.length - deleted];

            for(String s : argv){
                if( s != null ){
                    args[cnt++] = s;
                }
            }

            return args;
        }

        return new String[0];
    }

    // -xvzf == -x -v -z -f (concatenation of short bools)
    private boolean juxtaShort(String arg){
    
        char[] letter = arg.substring(1).toCharArray();
        String tmp;
        List<BoolOption> list = new ArrayList<BoolOption>(arg.length());

        for(int i = 0; i < letter.length; i++){
            tmp = "-"+letter[i];
            Option o = isOption(tmp);
            if (o == null || ! (o instanceof BoolOption)){
                return false;
            }else{
                list.add((BoolOption)o);
            }
        }

        // if we get to here they were all options

        for(BoolOption bo: list){
            bo.set();
        }

        return true;
    }

    @SuppressWarnings("unchecked")
    private boolean juxtaLong(String arg){

        int max = Integer.MIN_VALUE;
        String longestKey = "";
        StringOption so;
        Option o;

        for(String key: options.keySet()){

            if(arg.startsWith(key) && key.length() > max){
                o = options.get(key);
                if( o instanceof StringOption){
                    max = key.length();
                    longestKey = key;
                }
            }
        }

        if( max > Integer.MIN_VALUE ){
            so = (StringOption) options.get(longestKey);
            so.add( arg.substring( max ) );
            return true;
        }

        return false;
    }


    private Option isOption(String optstring){ return options.get(optstring);}

    /**
     * Report if an option was found during parsing.
     *
     * @param optstring identifies an option.
     * typically "-file", "-help" or so..
     * @return true if option was set, false otherwise.
     */
    public boolean isSet(String optstring){

        Option option = options.get(optstring);

        if ( option == null ){
            throw new RuntimeException("\nGetOpt.isSet( non-existent-option ) -> fail");
        }

        return option.isSet();
    }

    /**
     * Return argument of String option.
     *
     * @param optstring the option you want the value of.
     * @return argument of String option.
     */
    public String getValue(String optstring){

        if( ! isSet( optstring ) ){
            throw new RuntimeException("\nGetOpt.getValue( unset-option ) -> fail ");
        }

        Option option = options.get(optstring);

        if( option instanceof StringOption ){
            
            StringOption so = (StringOption) option;
            return so.getValue();
    
        }else{
            throw new RuntimeException("\nGetOpt.getValue( bool-option ) -> fail ");
        }
    }

    /**
     * Return multiple arguments of String option.
     *
     * @param optstring the option you want values of.
     * @return arguments of String option.
     */
    public String[] getValues(String optstring){

        if( ! isSet( optstring ) ){
            throw new RuntimeException("\nGetOpt.getValues( unset-option ) -> fail ");
        }

        Option option = options.get(optstring);

        if( option instanceof StringOption ){
            
            StringOption so = (StringOption) option;
            return so.getValues();
    
        }else{
            throw new RuntimeException("\nGetOpt.getValues( bool-option ) -> fail ");
        }
    }

    public String toString(){

        StringBuilder sb = new StringBuilder();
        HashSet<String> set = new HashSet<String>();

        sb.append("\n[GetOpt]\n");

        for(Option o : options.values()){
            set.add(o.toString());
        }

        for(String so : set){
            sb.append(so).append("\n");
        }

        return sb.toString();
    }
}
